/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;
using System;
using System.Windows.Media;
using Xceed.Silverlight.Compatibility;

namespace Xceed.Silverlight.DragDrop
{
  internal sealed class DragDropAdornerManager : DependencyObject
  {
    #region Singleton Implementation

    private static DragDropAdornerManager InstanceCache;

    public static DragDropAdornerManager Instance
    {
      get
      {
        if( DragDropAdornerManager.InstanceCache == null )
        {
          DragDropAdornerManager.InstanceCache = new DragDropAdornerManager();
        }

        return DragDropAdornerManager.InstanceCache;
      }
    }

    #endregion

    #region Constructors

    private DragDropAdornerManager()
    {
      m_draggedPopup = new Popup();
      m_draggedPopup.IsHitTestVisible = false;
    }

    #endregion

    #region DraggedElementImageSource Attached Property

    // Attached Property used by the Default
    internal static readonly DependencyProperty DraggedElementImageSourceProperty = DependencyProperty.RegisterAttached(
      "DraggedElementImageSource",
      typeof( ImageSource ),
      typeof( DragDropAdornerManager ),
      new PropertyMetadata( null ) );

    public static ImageSource GetDraggedElementImageSource( DependencyObject obj )
    {
      return ( ImageSource )obj.GetValue( DragDropAdornerManager.DraggedElementImageSourceProperty );
    }

    internal static void SetDraggedElementImageSource( DependencyObject obj, ImageSource value )
    {
      obj.SetValue( DragDropAdornerManager.DraggedElementImageSourceProperty, value );
    }

    #endregion

    #region ContentPresenterProvider Attached Property

    internal static readonly DependencyProperty ContentPresenterProviderProperty = DependencyProperty.RegisterAttached(
      "ContentPresenterProvider",
      typeof( IProvideDraggedContentPresenter ),
      typeof( DragDropAdornerManager ),
      new PropertyMetadata( ( IProvideDraggedContentPresenter )null ) );

    public static IProvideDraggedContentPresenter GetContentPresenterProvider( DependencyObject obj )
    {
      return ( IProvideDraggedContentPresenter )obj.GetValue( DragDropAdornerManager.ContentPresenterProviderProperty );
    }

    internal static void SetContentPresenterProvider( DependencyObject obj, IProvideDraggedContentPresenter value )
    {
      obj.SetValue( DragDropAdornerManager.ContentPresenterProviderProperty, value );
    }

    #endregion

    #region VisualRoot

    public UIElement RootVisual
    {
      get
      {
#if SILVERLIGHT
        return Application.Current.RootVisual;
#else
        return Application.Current.MainWindow;
#endif 
      }
    }

    #endregion VisualRoot


    public static void PrepareAdorner( DragDropContext dragContext,
      MouseEventArgs e )
    {
      DragDropAdornerManager.Instance.PrepareAdornerWorker( dragContext, e );
    }

    private void PrepareAdornerWorker( DragDropContext dragContext, MouseEventArgs e )
    {
      m_dragContext = dragContext;
      m_draggedElement = dragContext.DraggedElement as FrameworkElement;
      Debug.Assert( m_draggedElement != null );

      if( m_draggedElement == null )
        return;

      UIElement draggedElementRoot = dragContext.DraggedElementRoot;
      if( draggedElementRoot == null )
      {
        draggedElementRoot = this.RootVisual;
      }

      m_draggedElement.CaptureMouse();

      DraggedElementConfiguration configuration = dragContext.Configuration;
      bool positionAdornerRelativeToAdornedElement = true;

      if( configuration != null )
      {
        positionAdornerRelativeToAdornedElement = configuration.PositionAdornerRelativeToElement;
      }
      else
      {
        positionAdornerRelativeToAdornedElement = true;
      }

      // Get the position relative to the dragged element to be able to 
      // offset the dragged elements at the actual mouse position
      Point positionToDraggedElement = ( !positionAdornerRelativeToAdornedElement )
        ? e.GetPosition( m_draggedElement )
        : new Point();

      // Get the mouse position according to the application
      Point mousePosition = e.GetPosition( null );
      Point mousePositionToDraggedElementRoot = e.GetPosition( draggedElementRoot );
      m_initialMousePositionToDraggedElementRootX = mousePositionToDraggedElementRoot.X;
      m_initialMousePositionToDraggedElementRootY = mousePositionToDraggedElementRoot.Y;

      // Position the Popup at the top left corner of the ListBox
      // to be able to use the exact same RenderTransform as the
      // adornedElement
      m_draggedPopup.HorizontalOffset = mousePosition.X - m_initialMousePositionToDraggedElementRootX;
      m_draggedPopup.VerticalOffset = mousePosition.Y - m_initialMousePositionToDraggedElementRootY;

      // Ensure to stop the fade out animation and
      // to clean the DraggedPopup if a drag operation
      // was started before the fade out storyboard was
      // completed
      if( m_draggedPopup.Child != null )
      {
        if( m_fadeOutDraggedAdornersStoryboard != null )
          m_fadeOutDraggedAdornersStoryboard.Stop();

        this.ClearDraggedPopup();
      }

      // Create a new Canvas in the Popup since the 
      // Child is set to null when Drag is completed/aborted
      Canvas canvas = new Canvas();
      m_draggedPopup.Child = canvas;

      ContentPresenter draggedContentPresenter = this.CreateContentPresenterForDraggedElement( m_draggedElement,
        positionToDraggedElement,
        dragContext );

      canvas.Children.Add( draggedContentPresenter );

      IEnumerable<FrameworkElement> siblingDraggedElements = dragContext.SiblingDraggedElements;

      if( ( siblingDraggedElements != null ) && ( configuration.DisplaySiblingElements ) )
      {
        foreach( FrameworkElement siblingDraggedElement in siblingDraggedElements )
        {
          ContentPresenter siblingAdorner = this.CreateContentPresenterForDraggedElement( siblingDraggedElement,
          positionToDraggedElement,
          dragContext );

          canvas.Children.Add( siblingAdorner );
        }
      }

      this.FadeInDraggedAdorners( configuration );

      m_draggedPopup.IsOpen = true;
    }

    private ContentPresenter CreateContentPresenterForDraggedElement( FrameworkElement draggedElement,
      Point mousePositionToDraggedElement,
      DragDropContext dragContext )
    {
      DraggedElementConfiguration configuration = dragContext.Configuration;
      bool isDraggedElement = ( m_draggedElement == draggedElement );

      ContentPresenter contentPresenter = null;
      IProvideDraggedContentPresenter contentPresenterProvider = draggedElement as IProvideDraggedContentPresenter;

      if( contentPresenterProvider != null )
      {
        contentPresenter = contentPresenterProvider.CreateDraggedContentPresenter();
        contentPresenterProvider.PrepareDraggedContentPresenter( contentPresenter );
        DragDropAdornerManager.SetContentPresenterProvider( contentPresenter, contentPresenterProvider );
      }
      else
      {
        contentPresenter = new ContentPresenter();
      }

      UIElement draggedElementRoot = dragContext.DraggedElementRoot;
      if( draggedElementRoot != null )
      {
        draggedElementRoot = this.RootVisual;
      }

      // Correct dragged element root bias caused by the current browser's zoom factor
      Point draggedElementToRoot = draggedElement.TransformToVisual( draggedElementRoot ).Transform( new Point() );

#if WINDOWS_PHONE
#else
#if SILVERLIGHT
      draggedElementToRoot.X /= Application.Current.Host.Content.ZoomFactor;
      draggedElementToRoot.Y /= Application.Current.Host.Content.ZoomFactor;
#endif
#endif

      Point contentPresenterPositionBias = mousePositionToDraggedElement;

      if( dragContext.Configuration != null )
      {
        if( !dragContext.Configuration.ApplyDraggedElementRenderTransform )
        {
          // Remove scaling transforms
          contentPresenter.RenderTransform = this.ExtractNonScalingTransform( contentPresenter.RenderTransform );

          // Get draggedElement's Scale Factor
          double xScaleFactor = 1.0d;
          double yScaleFactor = 1.0d;
          TransformGroup transformGroup = draggedElement.RenderTransform as TransformGroup;
          if( transformGroup != null )
          {
            foreach( Transform child in transformGroup.Children )
            {
              ScaleTransform scaleTransform = child as ScaleTransform;
              {
                if( scaleTransform != null )
                {
                  xScaleFactor = scaleTransform.ScaleX;
                  yScaleFactor = scaleTransform.ScaleY;
                }
              }
            }
          }

          contentPresenterPositionBias = new Point( ( mousePositionToDraggedElement.X * xScaleFactor ), ( mousePositionToDraggedElement.Y * yScaleFactor ) );
        }
      }

      // Ensure to get the actual Left and Top values in case a element created via IProvideDraggedContentPresenter
      // affected them because RenderTransforms are used on the element.
      // We must adjust the translation of the element according to the mouse position
      // Also make adjustments to scaleFactor
      Canvas.SetLeft( contentPresenter, Canvas.GetLeft( contentPresenter ) + draggedElementToRoot.X + contentPresenterPositionBias.X );
      Canvas.SetTop( contentPresenter, Canvas.GetTop( contentPresenter ) + draggedElementToRoot.Y + contentPresenterPositionBias.Y );

      contentPresenter.IsHitTestVisible = false;

      if( configuration != null )
      {
        Binding binding = new Binding();
        binding.Mode = BindingMode.OneWay;
        binding.Source = configuration;
        binding.Path = ( isDraggedElement )
          ? new PropertyPath( "ElementTemplate" )
          : new PropertyPath( "SiblingElementTemplate" );

        contentPresenter.SetBinding( ContentPresenter.ContentTemplateProperty, binding );
      }

      if( dragContext.Data != null )
      {
        contentPresenter.Content = dragContext.Data;
      }
      // We can't assign the same DataContext if it is a UIElement
      // to avoid any exceptions of kind "Already the child of another
      // UIElement"
      else if( ( draggedElement.DataContext as UIElement ) == null )
      {
        contentPresenter.Content = draggedElement.DataContext;
      }

      // Set the DragContext on the adorner to be able to bind
      // to the any of its property
      DragDropContext.SetParentDragContext( contentPresenter, dragContext );

      // Set the DraggedElementAdorner AttachedProperty on the ContentPresenter
      // to be able to display the copy of the dragged element in an Image Control
      // if no ContentTemplate is explicitly defined on the DraggedElementConfiguration
      DragDropAdornerManager.SetDraggedElementImageSource( contentPresenter, CompatibilityUtils.CreateImageFrom( draggedElement ) );

      return contentPresenter;
    }

    private Transform ExtractNonScalingTransform( Transform transform )
    {
      // If Transform is scaling, cancel it
      if( transform is ScaleTransform )
      {
        return null;
      }

      // If Transform is a composite, remove scaling
#if SILVERLIGHT
      CompositeTransform composite = transform as CompositeTransform;
      if( composite != null )
      {
        CompositeTransform result = new CompositeTransform();

        result.CenterX = composite.CenterX;
        result.CenterY = composite.CenterY;

        result.Rotation = composite.Rotation;

        result.ScaleX = 1;
        result.ScaleY = 1;

        result.SkewX = composite.SkewX;
        result.SkewY = composite.SkewY;

        result.TranslateX = composite.TranslateX;
        result.TranslateY = composite.TranslateY;

        return result;
      }
#endif

      // If Transform is TransformGroup, remove scaling
      TransformGroup group = transform as TransformGroup;
      if( group != null )
      {
        TransformGroup result = new TransformGroup();

        foreach( Transform child in group.Children )
        {
          Transform newChild = ExtractNonScalingTransform( child );

          if( newChild != null )
          {
            result.Children.Add( newChild );
          }
        }

        return result;
      }

      // Otherwise, keep the transform as is
      return transform;
    }

    public static void OnAdornedElementMouseMove( MouseEventArgs e )
    {
      DragDropAdornerManager.Instance.OnAdornedElementMouseMoveWorker( e );
    }

    private void OnAdornedElementMouseMoveWorker( MouseEventArgs e )
    {
      Point actualMousePosition = e.GetPosition( null );
      m_draggedPopup.HorizontalOffset = actualMousePosition.X - m_initialMousePositionToDraggedElementRootX;
      m_draggedPopup.VerticalOffset = actualMousePosition.Y - m_initialMousePositionToDraggedElementRootY;
    }

    public static void HideAdorner()
    {
      DragDropAdornerManager.Instance.HideAdornerWorker();
    }

    private void HideAdornerWorker()
    {
      if( m_draggedElement != null )
      {
        m_draggedElement.ReleaseMouseCapture();
        m_draggedElement = null;
      }

      if( m_draggedPopup != null )
      {
        if( m_dragContext != null )
        {
          // Try to start the storyboard to fade out every adorners
          // if it is not possible, clean the DraggedPopup immediately
          if( this.FadeOutDraggedAdorners( m_dragContext.Configuration ) )
            return;
        }

        this.ClearDraggedPopup();
      }
    }

    private void FadeInDraggedAdorners( DraggedElementConfiguration configuration )
    {
      if( configuration == null )
        return;

      if( m_draggedPopup == null )
        return;

      Canvas canvas = m_draggedPopup.Child as Canvas;

      if( canvas == null )
        return;

      Storyboard fadeInDraggedAdornersStoryboard = new Storyboard();

      foreach( FrameworkElement child in canvas.Children )
      {
        DoubleAnimation opacityAnimation = new DoubleAnimation();
        opacityAnimation.From = 0d;
        opacityAnimation.To = 1d;
        opacityAnimation.Duration = configuration.GetDraggedElementFadeInDuration();

        Storyboard.SetTarget( opacityAnimation, child );
        Storyboard.SetTargetProperty( opacityAnimation, new PropertyPath( "Opacity" ) );
        fadeInDraggedAdornersStoryboard.Children.Add( opacityAnimation );
      }

      fadeInDraggedAdornersStoryboard.Begin();
    }

    private bool FadeOutDraggedAdorners( DraggedElementConfiguration configuration )
    {
      if( configuration == null )
        return false;

      if( m_draggedPopup == null )
        return false;

      Canvas canvas = m_draggedPopup.Child as Canvas;

      if( canvas == null )
        return false;

      if( m_fadeOutDraggedAdornersStoryboard != null )
        m_fadeOutDraggedAdornersStoryboard.Stop();

      m_fadeOutDraggedAdornersStoryboard = new Storyboard();

      foreach( UIElement child in canvas.Children )
      {
        DoubleAnimation opacityAnimation = new DoubleAnimation();
        opacityAnimation.To = 0d;
        opacityAnimation.Duration = m_dragContext.Configuration.GetDraggedElementFadeOutDuration();

        Storyboard.SetTarget( opacityAnimation, child );
        Storyboard.SetTargetProperty( opacityAnimation, new PropertyPath( "Opacity" ) );
        m_fadeOutDraggedAdornersStoryboard.Children.Add( opacityAnimation );
      }

      m_fadeOutDraggedAdornersStoryboard.Completed += this.OnFadeOutDraggedAdornersStoryboard_Completed;
      m_fadeOutDraggedAdornersStoryboard.Begin();
      return true;
    }

    private void OnFadeOutDraggedAdornersStoryboard_Completed( object sender, EventArgs e )
    {
      Storyboard storyboard = sender as Storyboard;

      if( storyboard != null )
        storyboard.Completed -= this.OnFadeOutDraggedAdornersStoryboard_Completed;

      this.ClearDraggedPopup();
    }

    private void ClearDraggedPopup()
    {
      if( m_draggedPopup == null )
        return;

      m_draggedPopup.IsOpen = false;

      Canvas canvas = m_draggedPopup.Child as Canvas;

      if( canvas == null )
        return;

      foreach( ContentPresenter child in canvas.Children )
      {
        IProvideDraggedContentPresenter provideDraggedContentPresenter = child.GetValue( DragDropAdornerManager.ContentPresenterProviderProperty ) as IProvideDraggedContentPresenter;

        child.ClearValue( DragDropAdornerManager.ContentPresenterProviderProperty );
        child.ClearValue( DragDropContext.ParentDragContextProperty );
        child.ClearValue( FrameworkElement.DataContextProperty );
        child.ClearValue( DragDropAdornerManager.DraggedElementImageSourceProperty );
        child.ClearValue( ContentPresenter.ContentTemplateProperty );

        if( provideDraggedContentPresenter != null )
          provideDraggedContentPresenter.ClearDraggedContentPresenter( child );
      }

      canvas.Children.Clear();

      m_draggedPopup.Child = null;
      m_draggedPopup.DataContext = null;

      m_dragContext = null;
    }

    #region Private Fields

    private Storyboard m_fadeOutDraggedAdornersStoryboard;
    private DragDropContext m_dragContext;
    private Popup m_draggedPopup;
    private FrameworkElement m_draggedElement;
    private double m_initialMousePositionToDraggedElementRootX;
    private double m_initialMousePositionToDraggedElementRootY;

    #endregion
  }
}
